{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Using batch prediction with Keras models on Cloud AI Platform\n",
    "\n",
    "## @tf.function decorator example"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Use a TensorFlow Enterprise 2.3 instance to run this notebook.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Define environment variables and create a storage bucket."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Change these to try this notebook out.\n",
    "BUCKET = 'your-project-id'\n",
    "PROJECT = 'your-project-id'\n",
    "REGION = 'us-central1'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "os.environ['BUCKET'] = BUCKET\n",
    "os.environ['PROJECT'] = PROJECT\n",
    "os.environ['REGION'] = REGION"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "if ! gsutil ls | grep -q gs://${BUCKET}/; then\n",
    "    gsutil mb -l ${REGION} gs://${BUCKET}\n",
    "fi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Import modules and prepare training data. (You use the [Boston Housing price regression dataset](https://keras.io/api/datasets/boston_housing/#load_data-function) as an example.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from pandas import DataFrame\n",
    "\n",
    "import tensorflow as tf\n",
    "from tensorflow.keras import layers, models, datasets"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "(x_train, y_train), (x_test, y_test) = datasets.boston_housing.load_data(\n",
    "    path=\"boston_housing.npz\", test_split=0.2, seed=113\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CRIM</th>\n",
       "      <th>ZN</th>\n",
       "      <th>INDUS</th>\n",
       "      <th>CHAS</th>\n",
       "      <th>NOX</th>\n",
       "      <th>RM</th>\n",
       "      <th>AGE</th>\n",
       "      <th>DIS</th>\n",
       "      <th>RAD</th>\n",
       "      <th>TAX</th>\n",
       "      <th>PTRATIO</th>\n",
       "      <th>B</th>\n",
       "      <th>LSTAT</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>count</th>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "      <td>404.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>mean</th>\n",
       "      <td>3.745111</td>\n",
       "      <td>11.480198</td>\n",
       "      <td>11.104431</td>\n",
       "      <td>0.061881</td>\n",
       "      <td>0.557356</td>\n",
       "      <td>6.267082</td>\n",
       "      <td>69.010644</td>\n",
       "      <td>3.740271</td>\n",
       "      <td>9.440594</td>\n",
       "      <td>405.898515</td>\n",
       "      <td>18.475990</td>\n",
       "      <td>354.783168</td>\n",
       "      <td>12.740817</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>std</th>\n",
       "      <td>9.240734</td>\n",
       "      <td>23.767711</td>\n",
       "      <td>6.811308</td>\n",
       "      <td>0.241238</td>\n",
       "      <td>0.117293</td>\n",
       "      <td>0.709788</td>\n",
       "      <td>27.940665</td>\n",
       "      <td>2.030215</td>\n",
       "      <td>8.698360</td>\n",
       "      <td>166.374543</td>\n",
       "      <td>2.200382</td>\n",
       "      <td>94.111148</td>\n",
       "      <td>7.254545</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>min</th>\n",
       "      <td>0.006320</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.460000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.385000</td>\n",
       "      <td>3.561000</td>\n",
       "      <td>2.900000</td>\n",
       "      <td>1.129600</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>188.000000</td>\n",
       "      <td>12.600000</td>\n",
       "      <td>0.320000</td>\n",
       "      <td>1.730000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>25%</th>\n",
       "      <td>0.081437</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>5.130000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.453000</td>\n",
       "      <td>5.874750</td>\n",
       "      <td>45.475000</td>\n",
       "      <td>2.077100</td>\n",
       "      <td>4.000000</td>\n",
       "      <td>279.000000</td>\n",
       "      <td>17.225000</td>\n",
       "      <td>374.672500</td>\n",
       "      <td>6.890000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>50%</th>\n",
       "      <td>0.268880</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>9.690000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.538000</td>\n",
       "      <td>6.198500</td>\n",
       "      <td>78.500000</td>\n",
       "      <td>3.142300</td>\n",
       "      <td>5.000000</td>\n",
       "      <td>330.000000</td>\n",
       "      <td>19.100000</td>\n",
       "      <td>391.250000</td>\n",
       "      <td>11.395000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>75%</th>\n",
       "      <td>3.674808</td>\n",
       "      <td>12.500000</td>\n",
       "      <td>18.100000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.631000</td>\n",
       "      <td>6.609000</td>\n",
       "      <td>94.100000</td>\n",
       "      <td>5.118000</td>\n",
       "      <td>24.000000</td>\n",
       "      <td>666.000000</td>\n",
       "      <td>20.200000</td>\n",
       "      <td>396.157500</td>\n",
       "      <td>17.092500</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>max</th>\n",
       "      <td>88.976200</td>\n",
       "      <td>100.000000</td>\n",
       "      <td>27.740000</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.871000</td>\n",
       "      <td>8.725000</td>\n",
       "      <td>100.000000</td>\n",
       "      <td>10.710300</td>\n",
       "      <td>24.000000</td>\n",
       "      <td>711.000000</td>\n",
       "      <td>22.000000</td>\n",
       "      <td>396.900000</td>\n",
       "      <td>37.970000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "             CRIM          ZN       INDUS        CHAS         NOX          RM  \\\n",
       "count  404.000000  404.000000  404.000000  404.000000  404.000000  404.000000   \n",
       "mean     3.745111   11.480198   11.104431    0.061881    0.557356    6.267082   \n",
       "std      9.240734   23.767711    6.811308    0.241238    0.117293    0.709788   \n",
       "min      0.006320    0.000000    0.460000    0.000000    0.385000    3.561000   \n",
       "25%      0.081437    0.000000    5.130000    0.000000    0.453000    5.874750   \n",
       "50%      0.268880    0.000000    9.690000    0.000000    0.538000    6.198500   \n",
       "75%      3.674808   12.500000   18.100000    0.000000    0.631000    6.609000   \n",
       "max     88.976200  100.000000   27.740000    1.000000    0.871000    8.725000   \n",
       "\n",
       "              AGE         DIS         RAD         TAX     PTRATIO           B  \\\n",
       "count  404.000000  404.000000  404.000000  404.000000  404.000000  404.000000   \n",
       "mean    69.010644    3.740271    9.440594  405.898515   18.475990  354.783168   \n",
       "std     27.940665    2.030215    8.698360  166.374543    2.200382   94.111148   \n",
       "min      2.900000    1.129600    1.000000  188.000000   12.600000    0.320000   \n",
       "25%     45.475000    2.077100    4.000000  279.000000   17.225000  374.672500   \n",
       "50%     78.500000    3.142300    5.000000  330.000000   19.100000  391.250000   \n",
       "75%     94.100000    5.118000   24.000000  666.000000   20.200000  396.157500   \n",
       "max    100.000000   10.710300   24.000000  711.000000   22.000000  396.900000   \n",
       "\n",
       "            LSTAT  \n",
       "count  404.000000  \n",
       "mean    12.740817  \n",
       "std      7.254545  \n",
       "min      1.730000  \n",
       "25%      6.890000  \n",
       "50%     11.395000  \n",
       "75%     17.092500  \n",
       "max     37.970000  "
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = DataFrame(x_train)\n",
    "df.columns = ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT']\n",
    "df.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create a model to predict average prices and train it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model: \"sequential\"\n",
      "_________________________________________________________________\n",
      "Layer (type)                 Output Shape              Param #   \n",
      "=================================================================\n",
      "hidden (Dense)               (None, 8)                 112       \n",
      "_________________________________________________________________\n",
      "output (Dense)               (None, 1)                 9         \n",
      "=================================================================\n",
      "Total params: 121\n",
      "Trainable params: 121\n",
      "Non-trainable params: 0\n",
      "_________________________________________________________________\n"
     ]
    }
   ],
   "source": [
    "model = models.Sequential()\n",
    "model.add(layers.Input(shape=(13,), name='features'))\n",
    "model.add(layers.Dense(8, activation='relu', name='hidden'))\n",
    "model.add(layers.Dense(1, activation=None, name='output'))\n",
    "model.summary()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.compile(optimizer='adam', loss='mse')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Training will take a few minutes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "history = model.fit(\n",
    "    x_train, y_train, validation_data=(x_test, y_test),\n",
    "    batch_size=64, epochs=1000, verbose=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<AxesSubplot:>"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "DataFrame({'train_loss': history.history['loss'], \n",
    "           'test_loss': history.history['val_loss']}).plot()\n",
    "DataFrame({'train_rmse': np.sqrt(history.history['loss']), \n",
    "           'test_rmse': np.sqrt(history.history['val_loss'])}).plot(ylim=[0, 15])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create a wrapper function so that you can add an identifier key to each example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "@tf.function(input_signature=[tf.TensorSpec([None, 13]), \n",
    "                              tf.TensorSpec([None], dtype=tf.int32)])\n",
    "def add_key(features, key):\n",
    "    pred = model(features, training=False)\n",
    "    return {'prediction': pred, 'key': key}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Export the model in the saved_model format using the wrapper function as 'serving_default'."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "WARNING:tensorflow:From /opt/conda/lib/python3.7/site-packages/tensorflow/python/training/tracking/tracking.py:111: Model.state_updates (from tensorflow.python.keras.engine.training) is deprecated and will be removed in a future version.\n",
      "Instructions for updating:\n",
      "This property should not be used in TensorFlow 2.0, as updates are applied automatically.\n",
      "WARNING:tensorflow:From /opt/conda/lib/python3.7/site-packages/tensorflow/python/training/tracking/tracking.py:111: Layer.updates (from tensorflow.python.keras.engine.base_layer) is deprecated and will be removed in a future version.\n",
      "Instructions for updating:\n",
      "This property should not be used in TensorFlow 2.0, as updates are applied automatically.\n",
      "INFO:tensorflow:Assets written to: ./export/assets\n"
     ]
    }
   ],
   "source": [
    "export_path = './export'\n",
    "model.save(export_path, signatures={'serving_default': add_key}, save_format='tf')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The given SavedModel SignatureDef contains the following input(s):\n",
      "  inputs['features'] tensor_info:\n",
      "      dtype: DT_FLOAT\n",
      "      shape: (-1, 13)\n",
      "      name: serving_default_features:0\n",
      "  inputs['key'] tensor_info:\n",
      "      dtype: DT_INT32\n",
      "      shape: (-1)\n",
      "      name: serving_default_key:0\n",
      "The given SavedModel SignatureDef contains the following output(s):\n",
      "  outputs['key'] tensor_info:\n",
      "      dtype: DT_INT32\n",
      "      shape: (-1)\n",
      "      name: StatefulPartitionedCall:0\n",
      "  outputs['prediction'] tensor_info:\n",
      "      dtype: DT_FLOAT\n",
      "      shape: (-1, 1)\n",
      "      name: StatefulPartitionedCall:1\n",
      "Method name is: tensorflow/serving/predict\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "saved_model_cli show --dir \"./export\" --tag_set serve --signature_def serving_default"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Make local predictions with the exported model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'key': <tf.Tensor: shape=(10,), dtype=int32, numpy=array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)>,\n",
       " 'prediction': <tf.Tensor: shape=(10, 1), dtype=float32, numpy=\n",
       " array([[ 8.973787],\n",
       "        [20.845522],\n",
       "        [22.98334 ],\n",
       "        [28.83278 ],\n",
       "        [25.87346 ],\n",
       "        [21.249987],\n",
       "        [28.717913],\n",
       "        [25.366228],\n",
       "        [18.829203],\n",
       "        [19.725237]], dtype=float32)>}"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "serving_fn = models.load_model(export_path).signatures['serving_default']\n",
    "serving_fn(features=tf.constant(x_test[:10].tolist()), key=tf.constant(range(10)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Deploy the exported model to AI Platform.\n",
    "\n",
    "**Note**: the latest runtime that supports batch prediction is 2.1\n",
    "\n",
    "https://cloud.google.com/ai-platform/training/docs/runtime-version-list"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Using endpoint [https://ml.googleapis.com/]\n",
      "Created ml engine model [projects/your-project-id/models/housing_price2].\n",
      "Using endpoint [https://ml.googleapis.com/]\n",
      "Creating version (this might take a few minutes)......\n",
      "...................................................................................................................................................................................................................................................................................................done.\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "MODEL_NAME=\"housing_price2\"\n",
    "MODEL_VERSION=\"v1\"\n",
    "MODEL_LOCATION=\"./export\"\n",
    "gcloud ai-platform models create ${MODEL_NAME} --regions $REGION\n",
    "gcloud ai-platform versions create ${MODEL_VERSION} --model ${MODEL_NAME} \\\n",
    "  --origin ${MODEL_LOCATION} --runtime-version 2.1 \\\n",
    "  --staging-bucket gs://$BUCKET --region global"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Make online predictions with the deployed model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\n",
      "    \"predictions\": [\n",
      "        {\n",
      "            \"key\": 0,\n",
      "            \"prediction\": [\n",
      "                8.973797798156738\n",
      "            ]\n",
      "        },\n",
      "        {\n",
      "            \"key\": 1,\n",
      "            \"prediction\": [\n",
      "                20.845523834228516\n",
      "            ]\n",
      "        }\n",
      "    ]\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "from googleapiclient import discovery\n",
    "from oauth2client.client import GoogleCredentials\n",
    "import json\n",
    "\n",
    "credentials = GoogleCredentials.get_application_default()\n",
    "api = discovery.build('ml', 'v1', credentials=credentials, cache_discovery=False)\n",
    "\n",
    "request_data =  {'instances':\n",
    "  [\n",
    "    {\"features\": [18.0846, 0, 18.1, 0, 0.679, 6.434, 100, 1.8347, 24, 666, 20.2, 27.25, 29.05], \"key\": 0},\n",
    "    {\"features\": [0.12329, 0, 10.01, 0, 0.547, 5.913, 92.9, 2.3534, 6.0, 432, 17.8, 394.95, 16.21], \"key\": 1},\n",
    "  ]\n",
    "}\n",
    "\n",
    "parent = 'projects/%s/models/%s/versions/%s' % (PROJECT, 'housing_price2', 'v1')\n",
    "response = api.projects().predict(body=request_data, name=parent).execute()\n",
    "print(json.dumps(response, sort_keys = True, indent = 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create an input file and execute a batch prediction job."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open('./input.json', 'w') as f:\n",
    "    for features, key in zip(x_test[:10].tolist(), np.arange(10).tolist()):\n",
    "        print(json.dumps({'features': features, 'key':key}), file=f)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\"features\": [18.0846, 0.0, 18.1, 0.0, 0.679, 6.434, 100.0, 1.8347, 24.0, 666.0, 20.2, 27.25, 29.05], \"key\": 0}\n",
      "{\"features\": [0.12329, 0.0, 10.01, 0.0, 0.547, 5.913, 92.9, 2.3534, 6.0, 432.0, 17.8, 394.95, 16.21], \"key\": 1}\n",
      "{\"features\": [0.05497, 0.0, 5.19, 0.0, 0.515, 5.985, 45.4, 4.8122, 5.0, 224.0, 20.2, 396.9, 9.74], \"key\": 2}\n",
      "{\"features\": [1.27346, 0.0, 19.58, 1.0, 0.605, 6.25, 92.6, 1.7984, 5.0, 403.0, 14.7, 338.92, 5.5], \"key\": 3}\n",
      "{\"features\": [0.07151, 0.0, 4.49, 0.0, 0.449, 6.121, 56.8, 3.7476, 3.0, 247.0, 18.5, 395.15, 8.44], \"key\": 4}\n",
      "{\"features\": [0.27957, 0.0, 9.69, 0.0, 0.585, 5.926, 42.6, 2.3817, 6.0, 391.0, 19.2, 396.9, 13.59], \"key\": 5}\n",
      "{\"features\": [0.03049, 55.0, 3.78, 0.0, 0.484, 6.874, 28.1, 6.4654, 5.0, 370.0, 17.6, 387.97, 4.61], \"key\": 6}\n",
      "{\"features\": [0.03551, 25.0, 4.86, 0.0, 0.426, 6.167, 46.7, 5.4007, 4.0, 281.0, 19.0, 390.64, 7.51], \"key\": 7}\n",
      "{\"features\": [0.09299, 0.0, 25.65, 0.0, 0.581, 5.961, 92.9, 2.0869, 2.0, 188.0, 19.1, 378.09, 17.93], \"key\": 8}\n",
      "{\"features\": [3.56868, 0.0, 18.1, 0.0, 0.58, 6.437, 75.0, 2.8965, 24.0, 666.0, 20.2, 393.37, 14.36], \"key\": 9}\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Copying file://./input.json [Content-Type=application/json]...\n",
      "/ [1 files][  1.1 KiB/  1.1 KiB]                                                \n",
      "Operation completed over 1 objects/1.1 KiB.                                      \n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "gsutil cp ./input.json gs://$BUCKET/input/input.json\n",
    "gsutil cat gs://$BUCKET/input/input.json"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "jobId: batch_pred_20210112121624\n",
      "state: QUEUED\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Job [batch_pred_20210112121624] submitted successfully.\n",
      "Your job is still active. You may view the status of your job with the command\n",
      "\n",
      "  $ gcloud ai-platform jobs describe batch_pred_20210112121624\n",
      "\n",
      "or continue streaming the logs with the command\n",
      "\n",
      "  $ gcloud ai-platform jobs stream-logs batch_pred_20210112121624\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "MODEL_NAME=\"housing_price2\"\n",
    "MODEL_VERSION=\"v1\"\n",
    "JOB_NAME=\"batch_pred_$(date +'%Y%m%d%H%M%S')\"\n",
    "INPUT_PATHS=\"gs://$BUCKET/input/*\"\n",
    "OUTPUT_PATH=\"gs://$BUCKET/output\"\n",
    "DATA_FORMAT=\"text\" # JSON data format\n",
    "gcloud ai-platform jobs submit prediction $JOB_NAME \\\n",
    "    --model $MODEL_NAME \\\n",
    "    --input-paths $INPUT_PATHS \\\n",
    "    --output-path $OUTPUT_PATH \\\n",
    "    --region $REGION \\\n",
    "    --data-format $DATA_FORMAT"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Open [Cloud Console](https://console.cloud.google.com/ai-platform/jobs) and wait for the batch prediction job to finish. When it has successfully completed, the prediction results are stored in the storage bucket."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!gsutil cat gs://$BUCKET/output/prediction.results*"
   ]
  }
 ],
 "metadata": {
  "environment": {
   "name": "tf2-gpu.2-3.m61",
   "type": "gcloud",
   "uri": "gcr.io/deeplearning-platform-release/tf2-gpu.2-3:m61"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
